<?php

namespace Fluency\Utils;

/**
 * A class that parses a string and retrieves annotation from it. Annotations are similar to
 * Javadoc tags, except that their names start with a capital letter and that they can optionally
 * have a JSON-encoded value which defaults to TRUE.
 *
 * Examples:
 *
 * <ul>
 *   <li>@AnAnnotation</li>
 *   <li>@AnotherAnnotation [1, 2, 3]</li>
 *   <li>@YetAnotherAnnotation false</li>
 * </ul>
 *
 * @package Fluency\Utils
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class AnnotationParser
{
    const ANNOTATION_PATTERN = '/\@(?<name>[A-Z][a-zA-Z0-9_]*)\s*(?<value>[^\*]+)/';

    protected $annotations = array();

    /**
     * The constructor.
     *
     * @param string $string a string to extract annotations from
     */
    public function __construct($string)
    {
        $this->parseAnnotations($string);
    }

    private function parseAnnotations($string)
    {
        preg_match_all(self::ANNOTATION_PATTERN, $string, $matches, PREG_SET_ORDER);
        foreach ($matches as $annotation) {
            $name = $annotation["name"];
            $value = trim($annotation["value"]);
            if (!isset($this->annotations[$name])) {
                $this->annotations[$name] = array();
            }
            $orig = $value;
            $value = !empty($value) ? json_decode($value, true) : true;
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \UnexpectedValueException(
                        "Extracted annotation value is not valid JSON: $orig");
            }
            $this->annotations[$name][] = $value;
        }
    }

    /**
     * Checks if there are any annotations in the parsed string.
     *
     * @return boolean
     */
    public function hasAnyAnnotations()
    {
        return !empty($this->annotations);
    }

    /**
     * Checks if there are any annotations of the given type in the parsed string.
     *
     * @param string $type
     * @return boolean
     */
    public function hasAnnotation($type)
    {
        return isset($this->annotations[$type]);
    }

    /**
     * Counts annotations of the given type in the parsed string.
     *
     * @param string $type
     * @return integer
     */
    public function countAnnotations($type)
    {
        return $this->hasAnnotation($type) ? count($this->annotations[$type]) : 0;
    }

    /**
     * Counts all annotations in the parsed string.
     *
     * @return integer
     */
    public function countAllAnnotations()
    {
        return array_sum(array_map("count", $this->annotations));
    }

    /**
     * Gets all annotations in the parsed string as a bidimensional array which has the annotation
     * names as the keys and arrays of annotation values as the values.
     *
     * @return mixed[][]
     */
    public function getAllAnnotations()
    {
        return $this->annotations;
    }

    /**
     * Gets the first annotation of the specified type or NULL if there are none.
     *
     * @param string $type
     * @return mixed
     */
    public function getAnnotation($type)
    {
        return $this->hasAnnotation($type) ? $this->annotations[$type][0] : null;
    }

    /**
     * Gets all the annotations of the specified type as an array.
     *
     * @param string $type
     * @return mixed[]
     */
    public function getAnnotations($type)
    {
        return $this->hasAnnotation($type) ? $this->annotations[$type] : array();
    }
}
